π fix(agent-launch): scrub backend-re-exported GitHub tokens from agent tool shells - #4048
Merged
Merged
Conversation
β¦nt tool shells Agent CLI backends re-export their own live GitHub credential as GITHUB_TOKEN into every shell they spawn for tool calls β after all launch-path scrubbing (#3931) has already run. Observed live on a Copilot-backed fleet: a wrapper-denied agent fell back to raw curl -H "Authorization: Bearer $GITHUB_TOKEN" and succeeded at a repo write, bypassing every gh-wrapper control (#3854 allowlist, mode/ACMM gates, merge eligibility, authorship routing, provenance). Fix at the only boundary that sees the re-export β CHILD shell startup: - bin/agent-env-scrub.sh (new): POSIX-safe unset of GITHUB_TOKEN, GH_TOKEN, GH_ENTERPRISE_TOKEN, GITHUB_ENTERPRISE_TOKEN, COPILOT_GITHUB_TOKEN, GITHUB_COPILOT_TOKEN, HIVE_GITHUB_TOKEN. - agent-launch.sh exports BASH_ENV/ENV pointing at it, so every non-interactive shell a backend spawns (including ones handed the token explicitly in the spawn env) scrubs itself before the agent's command runs. - Dockerfile ships the scrub and adds an /etc/bash.bashrc guard for interactive shells, gated on agent identity env so operator shells are untouched. The backend PROCESS keeps its own auth (it is not a shell and never sources the scrub): Copilot API auth via COPILOT_GITHUB_TOKEN and the opt-in app_authored_prs MCP token are unchanged. gh-wrapper and git-credential-hive keep authenticating from HIVE_AGENT_TOKEN_CACHE (a path, deliberately not scrubbed). bin/test_agent_env_scrub.sh (wired into v2-ci) replays the incident shape, asserts nested re-exported tokens are re-scrubbed, includes a positive control proving the probe can see a leak, a sanctioned-path control proving the wrapper still authenticates from the per-agent cache, and source-level drift guards. Residual: deliberate /proc/<pid>/environ extraction by a same-uid agent remains until proxy-side Authorization strip/injection (#1861) is enabled; this change composes with that work rather than replacing it. Fixes #4045 Signed-off-by: Andy Anderson <andy@clubanderson.com>
Contributor
|
[APPROVALNOTIFIER] This PR is NOT APPROVED This pull-request has been approved by: The full list of commands accepted by this bot can be found here. DetailsNeeds approval from an approver in each of these files:Approvers can indicate their approval by writing |
Contributor
|
Thank you for your contribution! Your PR has been merged. Check out what's new:
Stay connected: Slack #kubestellar-dev | Multi-Cluster Survey |
clubanderson
added a commit
that referenced
this pull request
Aug 26, 2026
β¦nt tool shells (#4048) Agent CLI backends re-export their own live GitHub credential as GITHUB_TOKEN into every shell they spawn for tool calls β after all launch-path scrubbing (#3931) has already run. Observed live on a Copilot-backed fleet: a wrapper-denied agent fell back to raw curl -H "Authorization: Bearer $GITHUB_TOKEN" and succeeded at a repo write, bypassing every gh-wrapper control (#3854 allowlist, mode/ACMM gates, merge eligibility, authorship routing, provenance). Fix at the only boundary that sees the re-export β CHILD shell startup: - bin/agent-env-scrub.sh (new): POSIX-safe unset of GITHUB_TOKEN, GH_TOKEN, GH_ENTERPRISE_TOKEN, GITHUB_ENTERPRISE_TOKEN, COPILOT_GITHUB_TOKEN, GITHUB_COPILOT_TOKEN, HIVE_GITHUB_TOKEN. - agent-launch.sh exports BASH_ENV/ENV pointing at it, so every non-interactive shell a backend spawns (including ones handed the token explicitly in the spawn env) scrubs itself before the agent's command runs. - Dockerfile ships the scrub and adds an /etc/bash.bashrc guard for interactive shells, gated on agent identity env so operator shells are untouched. The backend PROCESS keeps its own auth (it is not a shell and never sources the scrub): Copilot API auth via COPILOT_GITHUB_TOKEN and the opt-in app_authored_prs MCP token are unchanged. gh-wrapper and git-credential-hive keep authenticating from HIVE_AGENT_TOKEN_CACHE (a path, deliberately not scrubbed). bin/test_agent_env_scrub.sh (wired into v2-ci) replays the incident shape, asserts nested re-exported tokens are re-scrubbed, includes a positive control proving the probe can see a leak, a sanctioned-path control proving the wrapper still authenticates from the per-agent cache, and source-level drift guards. Residual: deliberate /proc/<pid>/environ extraction by a same-uid agent remains until proxy-side Authorization strip/injection (#1861) is enabled; this change composes with that work rather than replacing it. Fixes #4045 Signed-off-by: Andy Anderson <andy@clubanderson.com>
clubanderson
added a commit
that referenced
this pull request
Aug 26, 2026
β¦nt tool shells (#4048) Agent CLI backends re-export their own live GitHub credential as GITHUB_TOKEN into every shell they spawn for tool calls β after all launch-path scrubbing (#3931) has already run. Observed live on a Copilot-backed fleet: a wrapper-denied agent fell back to raw curl -H "Authorization: Bearer $GITHUB_TOKEN" and succeeded at a repo write, bypassing every gh-wrapper control (#3854 allowlist, mode/ACMM gates, merge eligibility, authorship routing, provenance). Fix at the only boundary that sees the re-export β CHILD shell startup: - bin/agent-env-scrub.sh (new): POSIX-safe unset of GITHUB_TOKEN, GH_TOKEN, GH_ENTERPRISE_TOKEN, GITHUB_ENTERPRISE_TOKEN, COPILOT_GITHUB_TOKEN, GITHUB_COPILOT_TOKEN, HIVE_GITHUB_TOKEN. - agent-launch.sh exports BASH_ENV/ENV pointing at it, so every non-interactive shell a backend spawns (including ones handed the token explicitly in the spawn env) scrubs itself before the agent's command runs. - Dockerfile ships the scrub and adds an /etc/bash.bashrc guard for interactive shells, gated on agent identity env so operator shells are untouched. The backend PROCESS keeps its own auth (it is not a shell and never sources the scrub): Copilot API auth via COPILOT_GITHUB_TOKEN and the opt-in app_authored_prs MCP token are unchanged. gh-wrapper and git-credential-hive keep authenticating from HIVE_AGENT_TOKEN_CACHE (a path, deliberately not scrubbed). bin/test_agent_env_scrub.sh (wired into v2-ci) replays the incident shape, asserts nested re-exported tokens are re-scrubbed, includes a positive control proving the probe can see a leak, a sanctioned-path control proving the wrapper still authenticates from the per-agent cache, and source-level drift guards. Residual: deliberate /proc/<pid>/environ extraction by a same-uid agent remains until proxy-side Authorization strip/injection (#1861) is enabled; this change composes with that work rather than replacing it. Fixes #4045 Signed-off-by: Andy Anderson <andy@clubanderson.com>
clubanderson
added a commit
that referenced
this pull request
Aug 26, 2026
β¦nt tool shells (#4048) Agent CLI backends re-export their own live GitHub credential as GITHUB_TOKEN into every shell they spawn for tool calls β after all launch-path scrubbing (#3931) has already run. Observed live on a Copilot-backed fleet: a wrapper-denied agent fell back to raw curl -H "Authorization: Bearer $GITHUB_TOKEN" and succeeded at a repo write, bypassing every gh-wrapper control (#3854 allowlist, mode/ACMM gates, merge eligibility, authorship routing, provenance). Fix at the only boundary that sees the re-export β CHILD shell startup: - bin/agent-env-scrub.sh (new): POSIX-safe unset of GITHUB_TOKEN, GH_TOKEN, GH_ENTERPRISE_TOKEN, GITHUB_ENTERPRISE_TOKEN, COPILOT_GITHUB_TOKEN, GITHUB_COPILOT_TOKEN, HIVE_GITHUB_TOKEN. - agent-launch.sh exports BASH_ENV/ENV pointing at it, so every non-interactive shell a backend spawns (including ones handed the token explicitly in the spawn env) scrubs itself before the agent's command runs. - Dockerfile ships the scrub and adds an /etc/bash.bashrc guard for interactive shells, gated on agent identity env so operator shells are untouched. The backend PROCESS keeps its own auth (it is not a shell and never sources the scrub): Copilot API auth via COPILOT_GITHUB_TOKEN and the opt-in app_authored_prs MCP token are unchanged. gh-wrapper and git-credential-hive keep authenticating from HIVE_AGENT_TOKEN_CACHE (a path, deliberately not scrubbed). bin/test_agent_env_scrub.sh (wired into v2-ci) replays the incident shape, asserts nested re-exported tokens are re-scrubbed, includes a positive control proving the probe can see a leak, a sanctioned-path control proving the wrapper still authenticates from the per-agent cache, and source-level drift guards. Residual: deliberate /proc/<pid>/environ extraction by a same-uid agent remains until proxy-side Authorization strip/injection (#1861) is enabled; this change composes with that work rather than replacing it. Fixes #4045 Signed-off-by: Andy Anderson <andy@clubanderson.com> Signed-off-by: Andy Anderson <andy@clubanderson.com>
clubanderson
added a commit
to gregoryhunt/hive
that referenced
this pull request
Sep 8, 2026
Merge commit (not squash) so both lineages stay intact on v5. Incoming from v4 (14 commits), notably: - hivecommons#4047 gh-wrapper label injection never fails the operation - hivecommons#4049 gh-wrapper author-gate via trusted bot-identity file - hivecommons#4048 agent-launch scrubs backend-re-exported GitHub tokens - hivecommons#4046 deleted-cwd pinned across three spawn sites - hivecommons#4040 project.issue_filter label gate + Labels tab - hivecommons#4051 de-materialize stale login_patterns - hivecommons#3898 default mode thresholds scaled by repo count - hivecommons#4035 openshift-netadmin SCC overlay - hivecommons#4028 hub commit-order resolve leak (coverage flake) v5 RFC line preserved: pkg/turn, pkg/toolapprove, the RFC design doc, the agent state inventory, and the v5 CI enablement. Signed-off-by: Andy Anderson <andy@clubanderson.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #4045
The re-export mechanism (investigated on v4 @ e379c7a)
Two lanes put a live
GITHUB_TOKENwithin an agent's shell reach, and both are downstream of every scrub #3931 added:/data/copilot-user-token, aghu_user-to-server token) or fromCOPILOT_GITHUB_TOKEN, and setsGITHUB_TOKENin the spawn environment of every shell it runs for tool calls. No parent-side unset can reach this: the token is injected per spawn, by the CLI layer, after all launch-path scrubbing has completed. This is the credential the wrapper-denied agent spent via rawcurl -H "Authorization: Bearer $GITHUB_TOKEN".github.app_authored_prs, opt-in):manager.goinjects the per-agent tier-scoped App token asGITHUB_TOKENinto the tmux session env of push-capable agents (agentEnvPairs, re-pushed hourly byrefreshAgentTokens) so Copilot's built-in GitHub MCP server authors as the App bot. Legitimate for the CLI process β but it inherits into every tool shell too, where even a tier-scoped token bypasses the wrapper's allowlist (π fix(gh-wrapper): deny-by-default allowlist for the general command surfaceΒ #3854), eligibility, and provenance gates.The fix: scrub at CHILD shell startup β the only boundary that sees the re-export
bin/agent-env-scrub.sh(new, POSIX/dash-safe, sourced-only): unsetsGITHUB_TOKEN,GH_TOKEN,GH_ENTERPRISE_TOKEN,GITHUB_ENTERPRISE_TOKEN,COPILOT_GITHUB_TOKEN,GITHUB_COPILOT_TOKEN,HIVE_GITHUB_TOKEN.agent-launch.shexportsBASH_ENV/ENVpointing at it before launching any backend, so every non-interactive shell in the agent's process tree β including one handed the token explicitly in its spawn env by the CLI β scrubs itself before the agent's command runs. Nested shells re-scrub (BASH_ENV stays exported down the tree).src/Dockerfileships the scrub and adds an/etc/bash.bashrcguard for interactive shells, gated on agent-identity env so operatorkubectl execshells are untouched.Per-backend auth disposition (audited per the issue's fleet-wide note)
COPILOT_GITHUB_TOKEN/user-token store for Copilot API; opt-inGITHUB_TOKENfor the built-in GitHub MCP serverCLAUDE_CODE_OAUTH_TOKEN(vendor)CODEX_HOMEauth (vendor)BOBSHELL_API_KEY, etc.)Sanctioned GitHub paths are unaffected and test-pinned: the gh wrapper sources the scrub at startup (losing only inherited token env it must never trust anyway β audit H3) and then exports
GH_TOKENitself fromHIVE_AGENT_TOKEN_CACHE(a path, deliberately not scrubbed; the realghis a Go binary and sources nothing);git-credential-hive.sh,hive-open-pr, andhive-mergeall read token caches directly.Relationship to PR #4032 (#1861 proxy-side credential injection)
#4032 is exactly the issue's preferred fix direction at the transport layer: under
HIVE_PROXY_INJECT_GH_AUTH=truethe MITM proxy strips any agent-suppliedAuthorizationheader and injects the hub-held scoped token, widening interception to all GitHub-family hosts. The two changes compose, with no file or mechanism overlap:/proc/<pid>/environ) and any smuggled credential β once the flag is on, nothing an agent holds authenticates anywhere.$GITHUB_TOKENthat agents demonstrably reach for the moment the wrapper says no β and it keeps mattering after the flip as the env-hygiene layer of defense in depth.Soak note for the #4032 flag-flip (not addressed here, by design): the Copilot CLI's own
api.github.comauth traffic rides the same identified-agent UID, so the strip/inject path may need a carve-out for backend self-auth; the shell scrub in this PR deliberately leaves the CLI process env alone and is unaffected either way.Companion incident context β friction and bypass must be fixed together
This gap formed one incident with the wrapper's fail-closed availability bugs: #4043 (edit-lane label injection failing entire operations β fix in flight in #4047) and #4044 (author-gate identity oracle). Every false wrapper denial is pressure toward the bypass; this leak is the capability the pressure finds. #4047 removes the pressure, this PR removes the ambient capability, and #4032 (operator-held) removes usable credentials from the agent's reach entirely. No file overlap with #4047; the two stack cleanly in either merge order.
Tests β
bin/test_agent_env_scrub.sh, wired into v2-ci18 assertions, per the security-gate doctrine (positive control first, sanctioned-path controls, source-level drift guards):
bash -cchild shows all 7 injected fake tokens β proving the probe can see a leak before anything asserts absence.env(1)) is token-less under the scrub;"Bearer $GITHUB_TOKEN"expands to"Bearer "; a token re-exported into a nested shell is scrubbed again.HIVE_AGENT_TOKEN_CACHE/ACMM/proxy vars pass through; the gh wrapper, run under the scrub with stale fake tokens inherited, reaches a stub gh authenticated with exactly the per-agent cache token.sh(dash).HIVE_AGENT_TOKEN_CACHEasserted absent from it;agent-launch.shBASH_ENV/ENV wiring, Dockerfile COPY, and the bashrc interactive arm are all source-asserted.No token material is real anywhere in the tests; probes assert presence/absence only. Residual risk (deliberate
/proc/<pid>/environextraction) is documented in the scrub header and closed by #4032's lane.π€ Generated with Claude Code